/**  
 * @date 2014年11月25日 下午1:53:26
 * @Copyright: 2014 unibroad.com Inc. All rights reserved.
 */
package com.strugglerz.dal.paging;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.strugglerz.model.common.CommonConstants;
import com.strugglerz.model.common.PageQuery;
import com.strugglerz.dal.paging.PagingDialect.Type;


/**
 * 为ibatis3提供基于方言(Dialect)的分页查询的插件 将拦截Executor.doQuery()方法实现分页方言的插入. 配置文件内容:
 * 
 * <pre>
 *  &lt;plugins>
 *  &lt;plugin interceptor="com.unibroad.touchus.user.center.common.mybatis.plugin.PaginationInterceptor">
 *      &lt;property name="dialectClass" value="com.unibroad.touchus.user.center.common.mybatis.dialect.MySQLDialect"/>
 *  &lt;/plugin>
 * &lt;/plugins>
 * </pre>
 * 
 * @author: suan@unibroad.com
 * @version $Id: PagingQueryInterceptor.java, v 0.1 2013-7-6 下午4:53:43 Asuan Exp
 */

@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class,
        Object.class, RowBounds.class, ResultHandler.class }) })
public class PagingQueryInterceptor implements Interceptor {

    private final Log logger = LogFactory.getLog(PagingQueryInterceptor.class);

    private String pagingDialect;

    private static final String PAGING_LIMIT_KEY = "limit";

    /**
     * @see org.apache.ibatis.plugin.Interceptor#intercept(org.apache.ibatis.plugin.Invocation)
     */
    @SuppressWarnings("rawtypes")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        final Object[] args = invocation.getArgs();

        MappedStatement mappedStatement = (MappedStatement) args[0];
        Object parameterObject = args[1];

        // 兼容 多参数并且有一是 Criteria 条件
        if (parameterObject instanceof Map && ((Map) parameterObject).size() > 1) {
            Map paramsMap = (Map) parameterObject;
            parameterObject = mutilParamWithCriteria(paramsMap);
        }

        BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);

        // 统一以 mysql limit 写法来拦截分页
        String originalSql = boundSql.getSql().trim();
        if (originalSql != null && !originalSql.toLowerCase().contains(PAGING_LIMIT_KEY)) {
            return invocation.proceed();
        }

        // ParamMap ,如果存在一个 PageQuery的参数，则走封装物理分页查询，
        HashMap parameterMap = (HashMap) parameterObject;
        PageQuery pageQuery = null;
        for (Object obj : parameterMap.values()) {
            if (obj instanceof PageQuery) {
                pageQuery = (PageQuery) obj;
                break;
            }
        }
        if (pageQuery == null) {
            return invocation.proceed();
        }

        Configuration configuration = mappedStatement.getConfiguration();

        // 根据分页查询sql统计总记录数,无记录,则直接返回空集
        int totalCount = getQueryTotalCount(mappedStatement, parameterMap, boundSql);
        if (totalCount < 1) {
            return new PageList<Object>();
        }

        // 有记录,区分库打印sql
        PagingDialect.Type databaseType = PagingDialect.Type.valueOf(StringUtils.upperCase(pagingDialect));
        if (databaseType == null) {
            throw new RuntimeException(
                    "the value of the pagingDialect property in configuration.xml is not defined!");
        }

        PagingDialect dialect = getDialectInstanceByType(databaseType);

        // 物理分页查询sql
        BoundSql boundSql4Log = new BoundSql(configuration, dialect.getPagingSql(originalSql),
                boundSql.getParameterMappings(), boundSql.getParameterObject());

        String pageSql = boundSql4Log.getSql();
        pageSql = StringUtils.replace(pageSql, "\n", " ").replaceAll("\t", " ");

        if (logger.isDebugEnabled()) {
            logger.debug("generated paging SQL " + pageSql);
        }

        Paginator paginator = new Paginator(pageQuery.getPageSize(), totalCount);
        paginator.setPage(pageQuery.getPageNumber());

        return new PageList((List) invocation.proceed(), paginator);
    }

    /**
     * 多参数及Criteria查询参数兼容设置，以便不需要共用mapper xml 配置
     * 
     * @param paramsMap
     * @return Map
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private Map mutilParamWithCriteria(Map paramsMap) {
        Collection paramVals = paramsMap.values();
        for (Object param : paramVals) {
            if (param != null && param.toString().contains("Criteria@")) {
                Field[] fields = param.getClass().getDeclaredFields();
                Method[] methods = param.getClass().getMethods();
                for (Field field : fields) {
                    for (Method method : methods) {
                        // 调用get或is等方法取对象值, 取没有参数的方法
                        if (StringUtils.endsWithIgnoreCase(method.getName(), field.getName())
                                && (method.getParameterTypes() == null
                                        || method.getParameterTypes().length < 1)) {
                            try {
                                paramsMap.put(field.getName(), method.invoke(param, new Object[] {}));
                            } catch (IllegalAccessException | IllegalArgumentException
                                    | InvocationTargetException e) {
                                logger.error("mutilParamWithCriteria exception", e);
                            }
                            break;
                        }
                    }
                }
                break;
            }
        }
        return paramsMap;
    }

    /**
     * 根据数据库类型，构建相应的查询方言
     * 
     * @param type
     *            数据库类型枚举
     * @return
     */
    private PagingDialect getDialectInstanceByType(Type type) {

            return new MysqlPagingDialect();
    }

    /**
     * 统计分页查询sql总数
     * 
     * @param mappedStatement
     * @param boundSql
     * @return
     * @throws SQLException
     */
    private int getQueryTotalCount(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql)
            throws SQLException {

        // jdbc 统计查询总数
        String counterSqlString = "select count(*) totalCount from"
                + StringUtils.substringBetween(boundSql.getSql().trim(), "from", "limit");

        counterSqlString = StringUtils.replace(counterSqlString, "\n", " ").replaceAll("\t", " ");

        // 表join 查询,基于ON 条件,互斥where
        // if (StringUtils.containsIgnoreCase(counterSqlString, " on ")
        // && StringUtils.containsIgnoreCase(counterSqlString, "WHERE")) {
        // int lastWhereIndex = StringUtils.lastIndexOfIgnoreCase(counterSqlString, "WHERE");
        // String lastWhereStr = StringUtils.substring(counterSqlString, lastWhereIndex);
        // lastWhereStr = StringUtils.replace(lastWhereStr, "WHERE", " AND ");
        // counterSqlString = StringUtils.substring(counterSqlString, 0, lastWhereIndex) +
        // lastWhereStr;
        // }

        if (logger.isDebugEnabled()) {
            logger.debug("paging query counter sql: " + counterSqlString);
        }

        Configuration configuration = mappedStatement.getConfiguration();

        // 统计记录sql绑定对象
        BoundSql countBoundSql = new BoundSql(configuration, counterSqlString,
                boundSql.getParameterMappings(), boundSql.getParameterObject());

        Iterator<ParameterMapping> paramMapItr = countBoundSql.getParameterMappings().iterator();
        ParameterMapping paramMapping = null;
        while (paramMapItr.hasNext()) {
            paramMapping = paramMapItr.next();
            String property = paramMapping.getProperty();
            // 去掉分页的参数信息
            if (StringUtils.endsWithIgnoreCase(property, "limit")
                    || StringUtils.endsWithIgnoreCase(property, "offset")) {
                paramMapItr.remove();
                continue;
            }

            // 补充 criteria 参数
            property = StringUtils.substringBeforeLast(property, CommonConstants.DOT_SEPARATOR);
            if (boundSql.getAdditionalParameter(property) != null) {
                countBoundSql.setAdditionalParameter(property, boundSql.getAdditionalParameter(property));
            }
        }

        Connection connection = configuration.getEnvironment().getDataSource().getConnection();
        PreparedStatement pstmt = connection.prepareStatement(counterSqlString);
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject,
                countBoundSql);
        parameterHandler.setParameters(pstmt);
        ResultSet resultset = pstmt.executeQuery();
        int totalCount = resultset != null && resultset.next() ? resultset.getInt(1) : 0;
        if (logger.isDebugEnabled()) {
            logger.debug("paging query total count: " + totalCount);
        }
        return totalCount;
    }

    /**
     * @see org.apache.ibatis.plugin.Interceptor#plugin(java.lang.Object)
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * @see org.apache.ibatis.plugin.Interceptor#setProperties(java.util.Properties)
     */
    @Override
    public void setProperties(Properties properties) {
        setPagingDialect(properties.getProperty("pagingDialect"));
    }

    /**
     * @return the pagingDialect
     */
    public String getPagingDialect() {

        return pagingDialect;
    }

    /**
     * @param pagingDialect
     *            the pagingDialect to set
     */
    public void setPagingDialect(String pagingDialect) {

        this.pagingDialect = pagingDialect;
    }

}
